﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace Silverlight
{
    /// <summary>
    /// Provides DataSet functionality
    /// </summary>
    public class DataSet : EventArgs, INotifyPropertyChanged
    {
        #region Static Variables
        /// <summary>
        /// Defines "msdata" prefix
        /// </summary>
        private static XNamespace msdata = "urn:schemas-microsoft-com:xml-msdata";

        /// <summary>
        /// Defines "xs" prefix
        /// </summary>
        private static XNamespace xs = "http://www.w3.org/2001/XMLSchema";
        #endregion

        #region Data Fields
        /// <summary>
        /// "HasErrors" property data field
        /// </summary>
        private bool _hasErrors;

        /// <summary>
        /// Holds DataSet schema
        /// </summary>
        private XElement _schema;

        /// <summary>
        /// "Tables" property data field
        /// </summary>
        private DataTableCollection _tables;
        #endregion

        #region Events
        /// <summary>
        /// INotifyPropertyChanged interface implementation
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        public DataSet()
        {
            this._tables = new DataTableCollection();
            this._tables.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(DataTableCollection_PropertyChanged);
            this._tables.SchemaChanged += new EventHandler<DataSetEventArgs>(DataTableCollection_SchemaChanged);
            this.CreateSchema();
        }

        /// <summary>
        /// Makes a new DataSet instance assigning name to it
        /// </summary>
        /// <param name="dataSetName">DataSet name</param>
        public DataSet(string dataSetName) : this()
        {
            // Validate argument
            if (string.IsNullOrEmpty(dataSetName))
            {
                throw new ArgumentNullException("dataSetName");
            }

            this.RenameSchema(dataSetName);
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets DataSet name
        /// </summary>
        public string DataSetName
        {
            get
            {
                return this._schema.Attribute("id").Value;
            }
            set
            {
                // Validate argument
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException("value");
                }

                this.RenameSchema(value);
            }
        }

        /// <summary>
        /// Gets "HasErrors" flag value
        /// </summary>
        public bool HasErrors
        {
            get
            {
                return this._hasErrors;
            }
        }

        /// <summary>
        /// Gets "HasNoErrors" flag value
        /// </summary>
        public bool HasNoErrors
        {
            get
            {
                return !this._hasErrors;
            }
        }

        /// <summary>
        /// Gets tables
        /// </summary>
        public DataTableCollection Tables
        {
            get
            {
                return this._tables;
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Commits all the changes made since the last time AcceptChanges was called
        /// </summary>
        public void AcceptChanges()
        {
            for (int i = 0; i < this._tables.Count; i++)
            {
                this._tables[i].AcceptChanges();
            }
        }

        /// <summary>
        /// Copies DataSet structure
        /// </summary>
        /// <returns>DataSet with copied structure</returns>
        public DataSet Clone()
        {
            DataSet dataSet = new DataSet(this.DataSetName);
            for (int i = 0; i < this._tables.Count; i++)
            {
                dataSet._tables.Add(this._tables[i].Clone(false));
            }

            return dataSet;
        }

        /// <summary>
        /// Copies DataSet structure and data
        /// </summary>
        /// <returns>DataSet with copied structure and data</returns>
        public DataSet Copy()
        {
            DataSet dataSet = new DataSet(this.DataSetName);
            for (int i = 0; i < this._tables.Count; i++)
            {
                dataSet._tables.Add(this._tables[i].Copy(false));
            }

            return dataSet;
        }

        /// <summary>
        /// Creates schema
        /// </summary>
        private void CreateSchema()
        {
            // Load schema
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append("<xs:schema id=\"DataSet\" xmlns=\"\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">");
            stringBuilder.Append("<xs:element name=\"DataSet\" msdata:IsDataSet=\"true\" msdata:UseCurrentLocale=\"true\">");
            stringBuilder.Append("<xs:complexType>");
            stringBuilder.Append("<xs:choice minOccurs=\"0\" maxOccurs=\"unbounded\">");
            stringBuilder.Append("</xs:choice>");
            stringBuilder.Append("</xs:complexType>");
            stringBuilder.Append("</xs:element>");
            stringBuilder.Append("</xs:schema>");
            this._schema = XElement.Parse(stringBuilder.ToString());
        }

        /// <summary>
        /// Deserializes data container from array of bytes
        /// </summary>
        /// <param name="data">Array of bytes</param>
        public void FromArray(byte[] data)
        {
            // Validate argument
            if (data == null)
            {
                throw new ArgumentNullException("data");
            }

            // Transform byte array into string
            string xmlData = null;
            using (MemoryStream memoryStream = new MemoryStream(data))
            {
                using (BinaryReader binaryReader = new BinaryReader(memoryStream))
                {
                    xmlData = binaryReader.ReadString();
                }
            }

            this.FromXml(xmlData);
        }

        /// <summary>
        /// Creates DataSet from xml
        /// </summary>
        /// <param name="data">Xml data</param>
				public void FromXml(string data) { FromXml(data, false); }
        public void FromXml(string data,bool schemaOnly)
        {
            // Validate argument
            if (string.IsNullOrEmpty(data))
            {
                throw new ArgumentNullException("data");
            }

            // Load xml data
            XElement xElement = XElement.Parse(data);
            XElement cachedSchema = XElement.Parse(xElement.FirstNode.ToString());

            // Create and load DataSet
            this._schema = (XElement)xElement.FirstNode;
            IEnumerable<XElement> tableSchemas = GetTableSchemas();
            foreach (XElement tableSchema in tableSchemas)
            {
                DataTable dataTable = new DataTable(tableSchema);
                this.Tables.Add(dataTable);
								if (schemaOnly) continue;
                foreach (XElement row in DataSet.GetRows(xElement, DataSet.GetTableName(tableSchema)))
                {
                    DataRow dataRow = dataTable.NewRow();
                    foreach (XElement field in row.Elements())
                    {
                        dataRow[field.Name.LocalName] = field.Value;
                    }

                    dataTable.Rows.Add(dataRow);
                }
            }

						if (!schemaOnly) {
							xElement.RemoveNodes();
							xElement.Add(cachedSchema);
						}
            this._schema = xElement;
        }

        /// <summary>
        /// Generates bindable objects
        /// </summary>
        /// <param name="connector">Connector instance</param>
        /// <returns>Collections of generated bindable objects</returns>
        public Dictionary<string, IList> GetBindableData(Connector connector)
        {
            Dictionary<string, IList> dictionary = new Dictionary<string, IList>();
            for (int i = 0; i < this._tables.Count; i++)
            {
                dictionary.Add(this._tables[i].TableName, this._tables[i].GetBindableData(connector));
            }

            return dictionary;
        }

        /// <summary>
        /// Gets DataSet without unchanged rows
        /// </summary>
        /// <returns>Copy of current DataSet without unchanged rows</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public DataSet GetChanges()
        {
            DataSet dataSet = this.Copy();
            for (int i = dataSet._tables.Count - 1; i >= 0; i--)
            {
                DataTable dataTable = dataSet._tables[i];

                // Process rows in original and copied tables
                for (int j = dataTable.Rows.Count - 1; j >= 0; j--)
                {
                    switch (dataTable.Rows[j].RowState)
                    {
                        case DataRowState.Unchanged:
                            // Delete unchanged row from original and copied tables
                            dataTable.Rows[j].Delete(true);
                            break;
                    }
                }
            }

            return dataSet;
        }

        /// <summary>
        /// Gets column schema
        /// </summary>
        /// <param name="tableSchema">Table schema</param>
        /// <param name="columnName">Column name</param>
        /// <returns>Column schema if found, otherwise null</returns>
        internal static XElement GetColumnSchema(XElement tableSchema, string columnName)
        {
            IEnumerable<XElement> columnSchemas = from Element in tableSchema.Descendants(xs + "element")
                   where Element.Attribute("name").Value == columnName
                   select Element;

            if (columnSchemas.Count() == 0)
            {
                return null;
            }

            return columnSchemas.Single();
        }

        /// <summary>
        /// Gets column schemas
        /// </summary>
        /// <param name="tableSchema">Table schema</param>
        /// <returns>Column schemas</returns>
        internal static IEnumerable<XElement> GetColumnSchemas(XElement tableSchema)
        {
            return from Element in tableSchema.Descendants(xs + "element")
                   select Element;
        }

        /// <summary>
        /// Prepares DataSet for submition
        /// </summary>
        /// <param name="getChanges">True for getting changes only</param>
        /// <returns>Copy of current DataSet with support tables</returns>
        private DataSet GetDataSetForSubmition(bool getChanges)
        {
            DataSet dataSet = null;
            if (getChanges)
            {
                dataSet = this.GetChanges();
            }
            else
            {
                dataSet = this.Copy();
            }

            for (int i = dataSet._tables.Count - 1; i >= 0; i--)
            {
                DataTable dataTable = dataSet._tables[i];

                // Add "RowIndex" and "RowState" columns
                DataColumn dataColumn = new DataColumn("RowIndex", Type.GetType("System.Int32"));
                dataTable.Columns.Add(dataColumn);
                dataColumn = new DataColumn("RowState", Type.GetType("System.String"));
                dataTable.Columns.Add(dataColumn);

                for (int j = dataTable.Rows.Count - 1; j >= 0; j--)
                {
                    DataRow dataRow = dataTable.Rows[j];
                    dataRow.Locked = true;

                    // Fill "RowIndex" and "RowState" fields
                    DataField dataField = dataRow.ItemArray["RowIndex"];
                    dataField.Value = dataRow.Index.ToString(CultureInfo.InvariantCulture);
                    dataField.AcceptChanges();
                    dataField = dataRow.ItemArray["RowState"];
                    dataField.Value = dataRow.RowState.ToString();
                    dataField.AcceptChanges();

                    // Call EndEdit on all rows
                    dataTable.Rows[j].EndEdit();
                }

                if (dataTable.Rows.Count > 0)
                {
                    // Make copy of DataTable
                    DataTable copiedDataTable = dataTable.Copy(true);

                    // Process rows in original and copied tables
                    for (int j = dataTable.Rows.Count - 1; j >= 0; j--)
                    {
                        switch (dataTable.Rows[j].RowState)
                        {
                            case DataRowState.Added:
                                // Delete unchanged row from original table
                                dataTable.Rows[j].Delete(true);
                                break;
                            case DataRowState.Deleted:
                                // Delete deleted row from copied table
                                copiedDataTable.Rows[j].Delete(true);
                                break;
                            case DataRowState.Modified:
                                // Reject changes for row, preserve it state for xml
                                DataRow dataRow = dataTable.Rows[j];
                                dataRow.RejectChanges();
                                DataField dataField = dataRow.ItemArray["RowState"];
                                dataField.Value = DataRowState.Modified.ToString();
                                dataField.AcceptChanges();
                                break;
                            case DataRowState.Unchanged:
                                // Delete unchanged row from copied table
                                copiedDataTable.Rows[j].Delete(true);
                                break;
                        }
                    }

                    // Add copied table to DataSet if it contains any record
                    if (copiedDataTable.Rows.Count > 0)
                    {
                        dataSet._tables.Add(copiedDataTable);
                    }
                }
                else
                {
                    if (getChanges)
                    {
                        this._tables.Remove(dataTable);
                    }
                }
            }

            return dataSet;
        }

        /// <summary>
        /// Gets all rows for specified table
        /// </summary>
        /// <param name="data">Table schema</param>
        /// <param name="tableName">Table name</param>
        /// <returns>All rows</returns>
        public static IEnumerable<XElement> GetRows(XElement data, string tableName)
        {
            return from Element in data.Descendants()
                   where Element.Name == tableName
                   select Element;
        }

        /// <summary>
        /// Gets table name
        /// </summary>
        /// <param name="tableSchema">Selected table</param>
        /// <returns>Table name</returns>
        internal static string GetTableName(XElement tableSchema)
        {
            return tableSchema.Attribute("name").Value;
        }

        /// <summary>
        /// Gets table schema
        /// </summary>
        /// <param name="tableName">Table name</param>
        /// <returns>Table shema if found, otherwise null</returns>
        internal XElement GetTableSchema(string tableName)
        {
            // Validate argument
            if (string.IsNullOrEmpty(tableName))
            {
                throw new ArgumentNullException("tableName");
            }

            // Validate required data
            if (this._schema == null)
            {
                throw new InvalidOperationException("Schema is missing");
            }

            IEnumerable<XElement> tableSchemas = from Element in this._schema.Descendants(xs + "element")
                                                 where Element.Attribute("type") == null
                                                 && Element.Attribute(msdata + "IsDataSet") == null
                                                 && Element.Attribute("name").Value == tableName
                                                 select Element;

            if (tableSchemas.Count() > 0)
            {
                return tableSchemas.Single();
            }

            return null;
        }

        /// <summary>
        /// Gets all table schemas
        /// </summary>
        /// <returns>Table shemas</returns>
        internal IEnumerable<XElement> GetTableSchemas()
        {
            // Validate required data
            if (this._schema == null)
            {
                throw new InvalidOperationException("Schema is missing");
            }

            return from Element in this._schema.Descendants(xs + "element")
                   where Element.Attribute("type") == null
                   && Element.Attribute(msdata + "IsDataSet") == null
                   select Element;
        }

        /// <summary>
        /// Gets value from specified table / row / column
        /// </summary>
        /// <param name="tableName">Table name</param>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string tableName, int rowIndex, string columnName, out bool? value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(tableName))
            {
                throw new ArgumentNullException("tableName");
            }

            // Validate object existence
            DataTable dataTable = this._tables[tableName];
            if (dataTable == null)
            {
                throw new ArgumentOutOfRangeException("tableName");
            }

            dataTable.GetValue(rowIndex, columnName, out value);
        }

        /// <summary>
        /// Gets value from specified table / row / column
        /// </summary>
        /// <param name="tableName">Table name</param>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string tableName, int rowIndex, string columnName, out DateTime? value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(tableName))
            {
                throw new ArgumentNullException("tableName");
            }

            // Validate object existence
            DataTable dataTable = this._tables[tableName];
            if (dataTable == null)
            {
                throw new ArgumentOutOfRangeException("tableName");
            }

            dataTable.GetValue(rowIndex, columnName, out value);
        }

        /// <summary>
        /// Gets value from specified table / row / column
        /// </summary>
        /// <param name="tableName">Table name</param>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string tableName, int rowIndex, string columnName, out int? value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(tableName))
            {
                throw new ArgumentNullException("tableName");
            }

            // Validate object existence
            DataTable dataTable = this._tables[tableName];
            if (dataTable == null)
            {
                throw new ArgumentOutOfRangeException("tableName");
            }

            dataTable.GetValue(rowIndex, columnName, out value);
        }

        /// <summary>
        /// Gets value from specified table / row / column
        /// </summary>
        /// <param name="tableName">Table name</param>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string tableName, int rowIndex, string columnName, out long? value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(tableName))
            {
                throw new ArgumentNullException("tableName");
            }

            // Validate object existence
            DataTable dataTable = this._tables[tableName];
            if (dataTable == null)
            {
                throw new ArgumentOutOfRangeException("tableName");
            }

            dataTable.GetValue(rowIndex, columnName, out value);
        }

        /// <summary>
        /// Gets value from specified table / row / column
        /// </summary>
        /// <param name="tableName">Table name</param>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(string tableName, int rowIndex, string columnName, out string value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(tableName))
            {
                throw new ArgumentNullException("tableName");
            }

            // Validate object existence
            DataTable dataTable = this._tables[tableName];
            if (dataTable == null)
            {
                throw new ArgumentOutOfRangeException("tableName");
            }

            dataTable.GetValue(rowIndex, columnName, out value);
        }

        /// <summary>
        /// Raises PropertChanged event
        /// </summary>
        /// <param name="propertyName">Property name</param>
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Recalculates errors
        /// </summary>
        private void RecalculateErrors()
        {
            // Prepare result variable
            bool hasErrors = false;

            // Check all tables
            for (int i = 0; i < this._tables.Count; i++)
            {
                if (this._tables[i].HasErrors)
                {
                    hasErrors = true;
                }
            }

            // Change "HasErrors" property if needed
            if (this._hasErrors != hasErrors)
            {
                this._hasErrors = hasErrors;
                this.OnPropertyChanged("HasErrors");
                this.OnPropertyChanged("HasNoErrors");
            }
        }

        /// <summary>
        /// Rejects all changes made since AcceptChanges was last called
        /// </summary>
        public void RejectChanges()
        {
            for (int i = 0; i < this._tables.Count; i++)
            {
                this._tables[i].RejectChanges();
            }
        }

        /// <summary>
        /// Renames schema
        /// </summary>
        /// <param name="dataSetName">DataSet name</param>
        private void RenameSchema(string dataSetName)
        {
            this._schema.Attribute("id").Value = dataSetName;
            this._schema.Element(xs + "element").Attribute("name").Value = dataSetName;
        }

        /// <summary>
        /// Sets value into specified table / row
        /// </summary>
        /// <param name="tableName">Table namr</param>
        /// <param name="rowIndex">Row index</param>
        /// <param name="values">Array of values</param>
        public void SetValues(string tableName, int rowIndex, object[] values)
        {
            for (int i = 0; i < values.Length; i++)
            {
                this.SetValue(tableName, rowIndex, i, values[i]);
            }
        }

        /// <summary>
        /// Sets value into specified table / row / filed
        /// Creates row if table has no rows or row index equals row count
        /// Throws an exception if table has rows but row index frate than row count
        /// </summary>
        /// <param name="tableName">Table name</param>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnIndex">Column index</param>
        /// <param name="value">Value for assigning</param>
        internal void SetValue(string tableName, int rowIndex, int columnIndex, object value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(tableName))
            {
                throw new ArgumentNullException("tableName");
            }

            // Get DataTable or create one if it doesn't exist
            DataTable dataTable = this._tables[tableName];
            if (dataTable == null)
            {
                throw new ArgumentOutOfRangeException("tableName");
            }

            dataTable.SetValue(rowIndex, columnIndex, value);
        }

        /// <summary>
        /// Sets value into specified table / row / filed
        /// Creates table if it doesn't exist
        /// Creates row if table has no rows or row index equals row count
        /// Throws an exception if table has rows but row index frate than row count
        /// Creates column if it doesn't exist
        /// </summary>
        /// <param name="tableName">Table name</param>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Value for assigning</param>
        /// <param name="valueType">Value type</param>
        public void SetValue(string tableName, int rowIndex, string columnName, object value)
        {
            // Validate arguments
            if (String.IsNullOrEmpty(tableName))
            {
                throw new ArgumentNullException("tableName");
            }

            // Get DataTable or create one if it doesn't exist
            DataTable dataTable = this._tables[tableName];
            if (dataTable == null)
            {
                dataTable = new DataTable(tableName);
                this._tables.Add(dataTable);
            }

            dataTable.SetValue(rowIndex, columnName, value);
        }


        /// <summary>
        /// Serializes DataSet into array of bytes
        /// </summary>
        /// <returns>Array of bytes</returns>
        public byte[] ToArray(bool getChanges)
        {
            byte[] data = null;
            using (MemoryStream memoryStream = new MemoryStream())
            {
                using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream))
                {
                    binaryWriter.Write(ToXml(getChanges));
                    data = memoryStream.ToArray();
                }
            }

            return data;
        }

        /// <summary>
        /// Creates xml representation of DataSet
        /// </summary>
        /// <param name="getChanges">True for getting changes only</param>
        /// <returns>Xml data with xsd schema</returns>
        public string ToXml(bool getChanges)
        {
            DataSet dataSet = this.GetDataSetForSubmition(getChanges);
            XElement dataSetXml = new XElement(this.DataSetName);
            dataSetXml.Add(dataSet._schema);
            for (int i = 0; i < dataSet._tables.Count; i++)
            {
                for (int j = 0; j < dataSet._tables[i].Rows.Count; j++)
                {
                    DataRow dataRow = dataSet._tables[i].Rows[j];
                    dataSetXml.Add(dataRow.XmlData);
                }
            }

            return dataSetXml.ToString();
        }
        #endregion

        #region Event Handlers
        /// <summary>
        /// Handles DataRowCollection.PropertyChanged event
        /// </summary>
        /// <param name="sender">Event source</param>
        /// <param name="e">Event arguments</param>
        void DataTableCollection_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "HasErrors")
            {
                this.RecalculateErrors();
            }
            else
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, e);
                }
            }
        }

        /// <summary>
        /// Handles Tables.SchemaChanged event
        /// </summary>
        /// <param name="sender">Event source</param>
        /// <param name="e">Event arguments</param>
        void DataTableCollection_SchemaChanged(object sender, DataSetEventArgs e)
        {
            DataTable dataTable = (DataTable)sender;
            switch (e.Command)
            {
                case "Add":
                    // Add column schema to table schema
                    XElement tableSchema = this.GetTableSchema(dataTable.TableName);
                    if (tableSchema == null)
                    {
                        this._schema.Element(xs + "element").Element(xs + "complexType").Element(xs + "choice").Add(dataTable.Schema);
                    }
                    break;
                case "Remove":
                    dataTable.Schema.Remove();
                    break;
            }
        }
        #endregion
    }
}
